import { addServerPlugin, createResolver, defineNuxtModule, useRuntimeConfig } from '@nuxt/kit'
import type { ModuleOptions } from '@nuxt/schema'
import { parse } from '@babel/parser'
import _traverse from '@babel/traverse'
import _generator from '@babel/generator'

interface INuxt3ViteLegacyOptions extends ModuleOptions {
  chrome?: number
  safari?: number
  edge?: number
  firefox?: number
}
// @ts-expect-error traverse7.x
const traverse = _traverse.default
// @ts-expect-error generator7.x
const generator = _generator.default
export default defineNuxtModule<INuxt3ViteLegacyOptions>({
  meta: { name: 'nuxt3-vite-legacy' },
  setup(options, nuxt) {
    if (nuxt.options.dev) return

    const shouldDelFiles: string[] = []
    // JS中含有注释时SystemJS会出错
    ;((((nuxt.options.vite ??= {}).build ??= {}).terserOptions ??= {}).output ??= {}).comments = false
    // 重复CSS
    nuxt.hook('vite:extendConfig', (viteConfig, { isClient }) => {
      if (isClient) {
        (viteConfig.plugins ??= []).push({
          name: 'legacy-css-repeat',
          generateBundle(outputOptions, bundle) {
            // region var
            const ARRAY = 'ArrayExpression'
            const FUNCTION = 'FunctionExpression'
            const OBJECT = 'ObjectExpression'
            const MEMBER = 'MemberExpression'
            const RETURN = 'ReturnStatement'
            const VAR = 'VariableDeclaration'
            const EXPRESSION = 'ExpressionStatement'
            const ASSIGN = 'AssignmentExpression'
            const CALL = 'CallExpression'
            const STRING = 'StringLiteral'
            const BINARY = 'BinaryExpression'
            // endregion
            for (const [fileName, chunk] of Object.entries(bundle)) {
              if (fileName.endsWith('-legacy.js') && chunk.type === 'chunk') {
                const removeStyle = (bodies: any[] = []) => {
                  if (bodies[0]?.type !== VAR) return
                  const styles = new Set<string>()
                  const declarations = bodies[0].declarations.filter((declaration: any) => {
                    if (
                      declaration.init?.type === CALL
                      && declaration.init?.callee?.object?.name === 'document'
                      && declaration.init?.callee?.property?.name === 'createElement'
                      && declaration.init?.arguments[0].value === 'style'
                    ) {
                      styles.add(declaration.id.name)
                      return false
                    }
                    return true
                  })
                  for (let i = 0; i < bodies.length; i++) {
                    if (!styles.size) break
                    if (bodies[i].type !== EXPRESSION) continue
                    const express = bodies[i].expression?.expressions || []
                    const { left = {}, right = {}, type } = express[0] || {}
                    const { property, object } = left
                    if (type !== ASSIGN
                      || property?.name !== 'textContent'
                      || (right.type !== STRING && right.type !== BINARY)
                      || !styles.has(object?.name)
                    ) continue
                    for (let j = 1; j < express.length; j++) {
                      const { arguments: arg = [], callee = {}, type } = express[j]
                      const { property: ppy, object: obj } = callee
                      if (type === CALL
                        && obj?.object?.name === 'document'
                        && obj?.property?.name === 'head'
                        && ppy?.name === 'appendChild'
                        && arg[0]?.name === object?.name) {
                        express.splice(j--, 1)
                        styles.delete(object?.name)
                      }
                    }
                    express.splice(0, 1)
                    if (!express.length) bodies.splice(i--, 1)
                  }
                  if (!(bodies[0].declarations = declarations).length) bodies.splice(0, 1)
                  if (!bodies.length) shouldDelFiles.push(fileName!.match(/[^/]*$/)?.[0] || '')
                }
                const ast = parse(chunk.code, { sourceType: 'script', plugins: ['dynamicImport'] })
                traverse(ast, {
                  CallExpression(path: any) {
                    const { callee = {}, arguments: args = [] } = path.node
                    const { type, object, property } = callee
                    if (type !== MEMBER || object?.name !== 'System' || property?.name !== 'register') return
                    const [params = {}, callback = {}] = args
                    if (callback.type !== FUNCTION) return
                    const { argument = {} } = callback.body?.body?.find((b: any) => b.type === RETURN) || {}
                    if (argument.type !== OBJECT) return
                    const execute = argument.properties?.find(({ key, value }: any) => key.name === 'execute' && value.type === FUNCTION)
                    if (!execute) return
                    removeStyle(execute.value.body.body)
                    if (params.type !== ARRAY || !shouldDelFiles.length) return
                    const setters = argument.properties?.find(({ key, value }: any) => key.name === 'setters' && value.type === ARRAY)
                    for (let i = 0, els = params.elements; i < els.length; i++) {
                      if (shouldDelFiles.includes(els[i].value.match(/[^/]*$/)?.[0])) {
                        els.splice(i, 1)
                        setters?.value?.elements?.splice(i, 1)
                        i--
                      }
                    }
                  },
                })
                chunk.code = generator(ast, {}, chunk.code).code
              }
            }
          },
        })
      }
    })

    // 初始化配置
    const { runtimeConfig, nitro } = nuxt.options
    const { chrome, safari, edge, firefox } = options
    const { dir = '.output', serverDir, publicDir } = nitro.output || {}
    const { chrome: _chrome, safari: _safari, edge: _edge, firefox: _firefox } = useRuntimeConfig().public
    if (_chrome || _safari || _edge || _firefox) {
      console.warn('\x1B[33m%s\x1B[0m', 'nuxt3-vite-legacy: The runtimeConfig.public.chrome/safari/edge/firefox configuration will soon be deprecated. It is recommended to use modules:[["nuxt3-vite-legacy",{edge:79,firefox:78,chrome:87,safari:12}]] instead.')
    }
    nuxt.options.runtimeConfig.public.nuxt3ViteLegacy = {
      ...(runtimeConfig.public.nuxt3ViteLegacy as object),
      dir,
      chrome: chrome || _chrome,
      safari: safari || _safari,
      edge: edge || _edge,
      firefox: firefox || _firefox,
      serverDir: serverDir || (dir + '/server'),
      publicDir: publicDir || (dir + '/public'),
    }

    nuxt.hook('build:manifest', async (manifest, isSelf?: boolean) => {
      if (isSelf === true) return
      // Nuxt3也注册了build:manifest，在这些hook里做资源处理(如从入口文件中剔除页面级别资源和全局组件)，但并不处理legacy相关的资源
      // nuxt3-vite-legacy在Nuxt3自身的hook之前执行，导致legacy资源和Nuxt3默认资源不一致
      // @ts-expect-error 强制Nuxt3自身hook先执行
      await nuxt.callHook('build:manifest', manifest, true)
      const [polyfill, legacy] = ['vite/legacy-polyfills', 'vite/legacy-polyfills-legacy']
      const transformToLegacy = (key = '') => key.replace(/(\.[^.]+$)/g, '-legacy$1')
      const exist = (key: string) => shouldDelFiles.some(file => key.includes(file))
      for (const [key, chunk] of Object.entries(manifest)) {
        if (key.includes('legacy')) {
          if (exist(key)) {
            delete manifest[key]
            continue
          }
          const originKey = key.replace(/-legacy(?=\.)/g, '')
          const origin = manifest[originKey] as Record<string, any>
          if (originKey !== key && origin) {
            origin._legacyFile = chunk.file
            if (origin.isEntry) {
              if (origin.dynamicImports) chunk.dynamicImports = origin.dynamicImports.map((o: string) => transformToLegacy(o))
              else delete chunk.dynamicImports
            }
          }
          else if (key === polyfill) {
            (chunk as any).isPolyfill = true
          }
          else if (key === legacy) {
            (chunk as any).isLegacy = true
          }
          if (shouldDelFiles.length > 0 && chunk.imports) {
            chunk.imports = chunk.imports.filter(ipt => !exist(ipt))
          }
          // 避免自动注入<link rel="modulepreload" ...>
          delete chunk.module
          // 避免自动注入<script src="xx-legacy.js" ...></script>
          delete chunk.isEntry
          delete chunk.css
        }
        if (chunk.name?.startsWith('vue.')) (chunk as any).isVue = true
      }
    })

    addServerPlugin(createResolver(import.meta.url).resolve('./runtime/plugin'))
  },
})
